Catalyst / admin/Strike 14.8 GB / 57.8 GB 40.0 GB free
Help Sign in

admin / Strike

public

Web-Based UK Cyber Compliance Tool with Reporting

Code Issues Pull requests Pipelines Packages Security Insights Wiki Settings
Strike / StrikeXi v3 / db / init / 01_schema.sql 12610 B · main
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
-- =====================================================================
-- StrikeXi v3 — Multi-Framework Cyber Maturity Assessment Platform
-- Relational schema: users, frameworks, taxonomy (objectives ->
-- principles), questions, assessments, answers, remediation mappings,
-- queued remediation actions, evidence/notes, score revisions, audit log.
--
-- v3 generalises the v2 (CAF-only) schema to support MULTIPLE maturity
-- frameworks (e.g. NCSC CAF, UK Cyber Security & Resilience Bill).
-- The taxonomy tables keep their `caf_*` names for continuity but are now
-- generic "framework level 1 (objective) -> level 2 (principle)" tables,
-- scoped by `framework_id`.
-- =====================================================================

CREATE EXTENSION IF NOT EXISTS "pgcrypto";

-- ---------- Frameworks (v3) ----------
-- A framework is a self-contained maturity model (its own objectives,
-- principles, questions and remediations). Adding a new framework is pure
-- data: insert a row here plus its taxonomy/question rows — no code change.
CREATE TABLE IF NOT EXISTS frameworks (
    id          TEXT PRIMARY KEY,            -- 'caf', 'csrb', ...
    name        TEXT NOT NULL,
    description TEXT,
    version     TEXT,
    sort_order  INT NOT NULL DEFAULT 0,
    is_active   BOOLEAN NOT NULL DEFAULT TRUE
);

-- ---------- Users & Authentication ----------
CREATE TABLE IF NOT EXISTS users (
    id                   UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    username             TEXT UNIQUE NOT NULL,
    password_hash        TEXT NOT NULL,
    full_name            TEXT,
    role                 TEXT NOT NULL DEFAULT 'assessor',  -- user | assessor | admin
    is_active            BOOLEAN NOT NULL DEFAULT TRUE,
    must_change_password BOOLEAN NOT NULL DEFAULT FALSE,
    mfa_secret           TEXT,                  -- base32 TOTP secret (null when no MFA)
    mfa_enabled          BOOLEAN NOT NULL DEFAULT FALSE,
    mfa_required         BOOLEAN NOT NULL DEFAULT FALSE,  -- admin-enforced MFA
    -- V2 self-service signup profile fields
    first_name           TEXT,
    surname              TEXT,
    company_name         TEXT,
    email                TEXT,
    contact_number       TEXT,
    -- How the account was created: 'self_service' (public signup) or 'admin'
    signup_origin        TEXT NOT NULL DEFAULT 'admin',
    -- True until the user passes their very first successful login (drives
    -- first-login MFA enforcement for self-service signups).
    first_login_pending  BOOLEAN NOT NULL DEFAULT FALSE,
    created_at           TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at           TIMESTAMPTZ NOT NULL DEFAULT now(),
    last_login           TIMESTAMPTZ
);

-- Idempotent migration for databases created before user-admin/MFA support.
ALTER TABLE users ADD COLUMN IF NOT EXISTS must_change_password BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE users ADD COLUMN IF NOT EXISTS mfa_secret   TEXT;
ALTER TABLE users ADD COLUMN IF NOT EXISTS mfa_enabled  BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE users ADD COLUMN IF NOT EXISTS mfa_required BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE users ADD COLUMN IF NOT EXISTS updated_at   TIMESTAMPTZ NOT NULL DEFAULT now();
ALTER TABLE users ADD COLUMN IF NOT EXISTS last_login   TIMESTAMPTZ;
-- V2 idempotent migrations
ALTER TABLE users ADD COLUMN IF NOT EXISTS first_name          TEXT;
ALTER TABLE users ADD COLUMN IF NOT EXISTS surname             TEXT;
ALTER TABLE users ADD COLUMN IF NOT EXISTS company_name        TEXT;
ALTER TABLE users ADD COLUMN IF NOT EXISTS email               TEXT;
ALTER TABLE users ADD COLUMN IF NOT EXISTS contact_number      TEXT;
ALTER TABLE users ADD COLUMN IF NOT EXISTS signup_origin       TEXT NOT NULL DEFAULT 'admin';
ALTER TABLE users ADD COLUMN IF NOT EXISTS first_login_pending BOOLEAN NOT NULL DEFAULT FALSE;

-- ---------- CAF Taxonomy: Objectives -> Principles ----------
-- Objectives: A (Managing security risk), B (Protecting against cyber attack),
--             C (Detecting cyber security events), D (Minimising impact)
CREATE TABLE IF NOT EXISTS caf_objectives (
    id           TEXT PRIMARY KEY,            -- 'A','B',... or 'CSRB-A' ...
    framework_id TEXT NOT NULL DEFAULT 'caf' REFERENCES frameworks(id),
    title        TEXT NOT NULL,
    description  TEXT,
    sort_order   INT NOT NULL DEFAULT 0
);

CREATE TABLE IF NOT EXISTS caf_principles (
    id           TEXT PRIMARY KEY,           -- 'A1','A2',... or 'CSRB-A1' ...
    framework_id TEXT NOT NULL DEFAULT 'caf' REFERENCES frameworks(id),
    objective_id TEXT NOT NULL REFERENCES caf_objectives(id),
    title        TEXT NOT NULL,
    description  TEXT,
    sort_order   INT NOT NULL DEFAULT 0
);

-- Idempotent migrations for databases created before v3 multi-framework support
ALTER TABLE caf_objectives ADD COLUMN IF NOT EXISTS framework_id TEXT NOT NULL DEFAULT 'caf';
ALTER TABLE caf_principles ADD COLUMN IF NOT EXISTS framework_id TEXT NOT NULL DEFAULT 'caf';

-- ---------- Questions & weighted answer options ----------
CREATE TABLE IF NOT EXISTS questions (
    id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    principle_id  TEXT NOT NULL REFERENCES caf_principles(id),
    code          TEXT UNIQUE NOT NULL,      -- e.g. 'A1.a-Q1'
    text          TEXT NOT NULL,
    -- CAF context/narrative explaining the basis of the question and what 'good' looks like
    guidance      TEXT,
    -- weight = relative importance of this question within its principle
    weight        NUMERIC(5,2) NOT NULL DEFAULT 1.0,
    sort_order    INT NOT NULL DEFAULT 0
);

-- Each answer option carries a score 0.0 - 1.0 (fraction of full marks)
CREATE TABLE IF NOT EXISTS answer_options (
    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    question_id UUID NOT NULL REFERENCES questions(id) ON DELETE CASCADE,
    label       TEXT NOT NULL,               -- 'Achieved','Partially','Not achieved'
    score       NUMERIC(4,3) NOT NULL,       -- 1.000 / 0.500 / 0.000
    sort_order  INT NOT NULL DEFAULT 0
);

-- ---------- Remediation library + mapping ----------
CREATE TABLE IF NOT EXISTS remediation_suggestions (
    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    title       TEXT NOT NULL,
    detail      TEXT NOT NULL,
    -- priority drives roadmap ordering (1 = do first)
    priority    INT NOT NULL DEFAULT 3,
    effort      TEXT NOT NULL DEFAULT 'medium'  -- low | medium | high
);

-- Map a principle (and a "fail" threshold) to a remediation.
-- When a principle's normalised score < threshold, the remediation is queued.
CREATE TABLE IF NOT EXISTS remediation_mappings (
    id             UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    principle_id   TEXT NOT NULL REFERENCES caf_principles(id),
    remediation_id UUID NOT NULL REFERENCES remediation_suggestions(id),
    threshold      NUMERIC(4,3) NOT NULL DEFAULT 0.700,  -- below => triggered
    UNIQUE (principle_id, remediation_id)
);

-- ---------- Assessments ----------
CREATE TABLE IF NOT EXISTS assessments (
    id                UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id           UUID NOT NULL REFERENCES users(id),
    framework_id      TEXT NOT NULL DEFAULT 'caf' REFERENCES frameworks(id),
    organisation_name TEXT NOT NULL,
    assessment_date   DATE NOT NULL DEFAULT CURRENT_DATE,
    status            TEXT NOT NULL DEFAULT 'in_progress', -- in_progress | completed
    overall_score     NUMERIC(5,2),        -- 0-100, latest computed score
    created_at        TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at        TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE assessments ADD COLUMN IF NOT EXISTS framework_id TEXT NOT NULL DEFAULT 'caf';

CREATE TABLE IF NOT EXISTS assessment_answers (
    id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    assessment_id UUID NOT NULL REFERENCES assessments(id) ON DELETE CASCADE,
    question_id   UUID NOT NULL REFERENCES questions(id),
    option_id     UUID NOT NULL REFERENCES answer_options(id),
    UNIQUE (assessment_id, question_id)
);

-- Snapshot of per-principle scores when an assessment is completed
CREATE TABLE IF NOT EXISTS assessment_principle_scores (
    id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    assessment_id UUID NOT NULL REFERENCES assessments(id) ON DELETE CASCADE,
    principle_id  TEXT NOT NULL REFERENCES caf_principles(id),
    score         NUMERIC(5,2) NOT NULL,    -- 0-100
    UNIQUE (assessment_id, principle_id)
);

-- Snapshot of per-objective scores when an assessment is completed
CREATE TABLE IF NOT EXISTS assessment_objective_scores (
    id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    assessment_id UUID NOT NULL REFERENCES assessments(id) ON DELETE CASCADE,
    objective_id  TEXT NOT NULL REFERENCES caf_objectives(id),
    score         NUMERIC(5,2) NOT NULL,    -- 0-100
    UNIQUE (assessment_id, objective_id)
);

-- Queued corrective actions (the future ITSM push target)
CREATE TABLE IF NOT EXISTS remediation_actions (
    id             UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    assessment_id  UUID NOT NULL REFERENCES assessments(id) ON DELETE CASCADE,
    principle_id   TEXT NOT NULL REFERENCES caf_principles(id),
    remediation_id UUID NOT NULL REFERENCES remediation_suggestions(id),
    principle_score NUMERIC(5,2) NOT NULL,
    status         TEXT NOT NULL DEFAULT 'queued', -- queued | exported | done
    -- Future ITSM integration fields (Jira / ServiceNow)
    external_ref   TEXT,
    external_system TEXT,
    created_at     TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- ---------- Evidence & notes (v3) ----------
-- Free-text notes and/or uploaded evidence files attached to an assessment,
-- either at the assessment level (question_id NULL) or against a specific
-- question. Every row is date/time-stamped and records the acting user;
-- each addition is also written to the audit log.
CREATE TABLE IF NOT EXISTS assessment_evidence (
    id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    assessment_id UUID NOT NULL REFERENCES assessments(id) ON DELETE CASCADE,
    question_id   UUID REFERENCES questions(id),   -- NULL = assessment-level
    kind          TEXT NOT NULL DEFAULT 'note',    -- note | evidence(file)
    content       TEXT,                            -- note text / file description
    file_name     TEXT,                            -- original filename (evidence)
    file_path     TEXT,                            -- stored path in evidence volume
    mime_type     TEXT,
    file_size     BIGINT,
    created_by    TEXT NOT NULL,                   -- acting username
    created_at    TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- ---------- Score revisions (v3) ----------
-- Immutable snapshot of an assessment's scores + answers taken every time it
-- is scored. Revision 1 = first completion; each re-score appends a new
-- revision so the ORIGINAL result is always retained for the audit trail and
-- the report can show previous-vs-new maturity.
CREATE TABLE IF NOT EXISTS assessment_revisions (
    id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    assessment_id UUID NOT NULL REFERENCES assessments(id) ON DELETE CASCADE,
    revision_no   INT NOT NULL,
    label         TEXT,
    overall_score NUMERIC(5,2),
    snapshot      JSONB NOT NULL,           -- objective/principle scores, answers, counts
    triggered_by  TEXT NOT NULL,            -- acting username
    created_at    TIMESTAMPTZ NOT NULL DEFAULT now(),
    UNIQUE (assessment_id, revision_no)
);

-- ---------- Audit log ----------
CREATE TABLE IF NOT EXISTS audit_log (
    id         BIGSERIAL PRIMARY KEY,
    ts         TIMESTAMPTZ NOT NULL DEFAULT now(),
    username   TEXT,                  -- may be null on failed login of unknown user
    action     TEXT NOT NULL,         -- LOGIN_SUCCESS, LOGIN_FAILED, ASSESSMENT_STARTED ...
    detail     TEXT,
    ip_address TEXT
);

-- Helpful indexes
CREATE INDEX IF NOT EXISTS idx_assessments_user ON assessments(user_id);
CREATE INDEX IF NOT EXISTS idx_answers_assessment ON assessment_answers(assessment_id);
CREATE INDEX IF NOT EXISTS idx_actions_assessment ON remediation_actions(assessment_id);
CREATE INDEX IF NOT EXISTS idx_evidence_assessment ON assessment_evidence(assessment_id);
CREATE INDEX IF NOT EXISTS idx_revisions_assessment ON assessment_revisions(assessment_id);
CREATE INDEX IF NOT EXISTS idx_objectives_framework ON caf_objectives(framework_id);
CREATE INDEX IF NOT EXISTS idx_principles_framework ON caf_principles(framework_id);
CREATE INDEX IF NOT EXISTS idx_audit_ts ON audit_log(ts DESC);